<template>
  <div id="app">
    <transition :name="transitionName">
      <keep-alive :include="$store.state.cachedViews">
        <router-view :key="$route.fullPath" />
      </keep-alive>
    </transition>
  </div>
</template>
<script>
export default {
  data() {
    return {
      transitionName: '',
    };
  },
  watch: {
    $route(to, from) {
      // 先区分【浏览器直接访问下级页面】与【由首页逐步进入下级页面】：
      // 如果直接访问下级页面则不应发生动画。判断特征是：from.name是null
      // （如果从首页进入下级页面，则from.name会是路由表里写的'Home'）
      if (from.name === null) {
        // 直接访问下级页面则设homeIsLandingPage为false，这样导航条后退按钮就不再执行go(-1)，而是push到首页
        this.$store.commit('SET_HOME_IS_LANDING_PAGE', false);
        return;
      }

      // 构建historyPages、确定路由方向、确定动画方向
      let fromInHistory = this.$store.state.historyPages.find((p) => p.fullPath === from.fullPath);
      if (!fromInHistory) {
        // 1被首页保留，所以从2开始
        fromInHistory = { fullPath: from.fullPath, index: 2 };
        this.$store.commit('SET_HISTORY_PAGES', fromInHistory);
      }

      let toInHistory = this.$store.state.historyPages.find((p) => p.fullPath === to.fullPath);
      // 新页未访问过
      if (!toInHistory) {
        // 新页面的index是原页面的index加1
        toInHistory = { fullPath: to.fullPath, index: fromInHistory.index + 1 };
        this.$store.commit('SET_HISTORY_PAGES', toInHistory);
        this.transitionName = 'slide-in';
      }
      // 新页访问过，且是回退操作
      else if (fromInHistory.index > toInHistory.index) {
        this.transitionName = 'slide-out';
      }
      // 新页访问过，且是前进操作
      else {
        this.transitionName = 'slide-in';
      }

      // 给原页面保存ScrollY，值是当前页面滚动条的位置
      this.$store.commit('SET_HISTORY_PAGES', { fullPath: from.fullPath, scrollY: document.documentElement.scrollTop });

      // 这里涉及到保持新页面滚动位置的原理：
      // 首先，以新页面的滚动位置为准，也就是说，让滚动条瞬移到新页面的滚动位置，
      // 然后让原页面迁就新页面的滚动位置，这时候原页面的滚动位置一定是错的，所以再给原页面设margin-top，以抵消错误
      // 最终从视觉上就会发现：原、新页面的滚动位置都没有变。

      // 发生回退或需保持滚动条的前进
      if (
        this.transitionName === 'slide-out' ||
        (this.transitionName === 'slide-in' && to.params.keepScrollY && toInHistory.scrollY)
      ) {
        // 注：原页面永远是#app的第一个子div，新页面永远是第二个子div
        document.querySelector('#app>div:nth-child(1)').style.marginTop =
          toInHistory.scrollY - document.documentElement.scrollTop + 'px';
        document.documentElement.scrollTop = toInHistory.scrollY;
      }
      // 发生不需保持滚动条的前进
      else {
        document.querySelector('#app>div').style.marginTop = 0 - document.documentElement.scrollTop + 'px';
        document.documentElement.scrollTop = 0;
      }
    },
  },
};
</script>

<style lang="scss">
body {
  margin: 0;
}

#app {
  /* 独立BFC */
  overflow: auto;
}

/*
 * 这个div负责等待Vue添加动画class
 * 这个div的内部结构应是：
 * .page-container负责非fixed的内容
 * .page-container外部可以有一个或多个fixed元素，其宽度必须为100vw，否则translate的时候会出错
 */
#app > div {
  /* 独立BFC */
  overflow: auto;
  /*
   * 第一个子div是原页面，需要用margin-top修正滚动位置，第二个子div是新页面，不需要修正，所以锁死为0
   * keep-alive前提下会保存原页面的margin-top，所以用下方代码锁死为0，即：
   * 只有一个子div时，:nth-last-child(1)就是这个div，要锁死为0
   * 有两个子div时，:nth-last-child(1)是倒数第一个，也即正数第二个子div，也要锁死为0
   */
  &:nth-last-child(1) {
    margin-top: 0 !important;
  }
}

/*
 * 这个div应是上面div的子元素，除首页外其他页面必须使用这个class，它负责包裹页面内所有不是fixed的元素
 * 由于首页永远没有动画，所以首页HTML可以自由写，不受此限制
 */
.page-container {
  /* 46px是顶部导航的高度 */
  padding-top: 46px;
  min-height: calc(100vh - 46px);
  padding-left: 15px;
  padding-right: 15px;
  /* 独立BFC */
  overflow: auto;
  background-color: #fff;
}

/*
 * 向左滑动（Slide In）
 */

/* 
 * 今日头条流派：由于原页面没有动画，所以不需下方keyframes
 * 微信流派：须使用下方keyframes
 */
// @keyframes leftViewWhenSlideIn {
//   from {
//     transform: translate3d(0, 0, 0);
//   }
//   to {
//     transform: translate3d(-100%, 0, 0);
//   }
// }
@keyframes rightViewWhenSlideIn {
  from {
    transform: translate3d(100%, 0, 0);
  }
  to {
    transform: translate3d(0, 0, 0);
  }
}
@keyframes leftViewWhenSlideIn-After {
  from {
    opacity: 0;
  }
  to {
    opacity: 0.4;
  }
}

#app {
  // 通常0.3秒较好，你可以修改为其他时间
  --slide-in-duration: 0.3s;
  --slide-out-duration: 0.3s;
}

.slide-in-leave-active {
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: fit-content;
  animation: leftViewWhenSlideIn var(--slide-in-duration) ease-out forwards;
}
/* 让原页面导航条不遮挡新页面导航条 */
.slide-in-leave-active > .van-nav-bar {
  z-index: 0;
}
/* 仿今日头条APP，给原页面的加上渐黑遮罩 */
.slide-in-leave-active::after {
  content: '';
  position: absolute;
  background-color: #000;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100%;
  animation: leftViewWhenSlideIn-After var(--slide-in-duration) ease-out forwards;
}
/*
 * fixed元素有一个特点是：祖先任一元素发生translate动画，则fixed就以该祖先为定位祖先。
 * 这就发生一个意外问题，动画过程中fixed元素不再基于视口定位，由于有滚动条的关系，fixed元素会跑到上方去。
 * 解决方案是，不给fixed元素的祖先设置translate，而是给fixed元素本身设置translate，同时也给其他元素设置translate，
 * 现在等于是分别动画，并行发生。
*/
.slide-in-enter-active {
  /* 虽然div自身不移动、只是子元素移动，但是Vue工作原理是读取自身元素的animation时间，所以这里写一个虚假动画，提供动画时间 */
  /* 下面代码中不再重复解释 */
  animation: nothing var(--slide-in-duration) ease-out forwards;
}
/* 凡fixed定位的元素都必须专门声明，且元素必须放在.page-container外面
 * 如果有些fixed元素只能写在.page-container里面，这时候确实不好办，只能让用户先点击“X符号”关掉元素，下一步再让用户跳转页面
 */
.slide-in-enter-active > .van-nav-bar {
  animation: rightViewWhenSlideIn var(--slide-in-duration) ease-out forwards;
}
.slide-in-enter-active > .page-container {
  /* 页面边缘阴影 */
  box-shadow: -2px 0px 2px 4px rgba(0, 0, 0, 0.2);
  /* 防止动画过程中被误点击 */
  /* 下面代码中不再重复解释 */
  pointer-events: none;
  animation: rightViewWhenSlideIn var(--slide-in-duration) ease-out forwards;
}

/*
 * 向右滑动（Slide Out）
 */

/*
 * 今日头条流派：由于原页面没有动画，所以删除下方keyframes
 * 微信流派：使用下方keyframes
 */
// @keyframes leftViewWhenSlideOut {
//   from {
//     transform: translate3d(-100%, 0, 0);
//   }
//   to {
//     transform: translate3d(0, 0, 0);
//   }
// }
@keyframes rightViewWhenSlideOut {
  from {
    transform: translate3d(0, 0, 0);
  }
  to {
    // 使用102%而不是100%，是因为页面左侧有阴影，如果使用100，阴影是原地消失的，而不是平移消失的，显得不够自然
    transform: translate3d(102%, 0, 0);
  }
}
/*
 * 今日头条流派：由于原页面没有动画，所以删除下方.slide-out-enter-active代码不会影响效果（当然也可保留）
 * 微信流派：须使用下方keyframes
 */
// .slide-out-enter-active {
//   pointer-events: none;
//   animation: leftViewWhenSlideOut var(--slide-out-duration) ease-out forwards;
// }
.slide-out-leave-active {
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: fit-content;
  /* 由于原页面的Z轴比新页面的Z轴更下层，但原页面应显示在新页面Z轴更上层，所以用z-index纠正 */
  z-index: 2;
  pointer-events: none;
  animation: nothing var(--slide-out-duration) ease-out forwards;
}

.slide-out-leave-active > .van-nav-bar {
  animation: rightViewWhenSlideOut var(--slide-out-duration) ease-out forwards;
}
.slide-out-leave-active > .page-container {
  box-shadow: -2px 0 2px 4px rgba(0, 0, 0, 0.2);
  animation: rightViewWhenSlideOut var(--slide-out-duration) ease-out forwards;
}
</style>
